/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.javapoet;

import static com.squareup.javapoet.Util.checkArgument;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collector;
import java.util.stream.StreamSupport;

import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;

/**
 * A fragment of a .java file, potentially containing declarations, statements,
 * and documentation. Code blocks are not necessarily well-formed Java code, and
 * are not validated. This class assumes javac will check correctness later!
 *
 * <p>
 * Code blocks support placeholders like {@link java.text.Format}. Where
 * {@link String#format} uses percent {@code %} to reference target values, this
 * class uses dollar sign {@code $} and has its own set of permitted
 * placeholders:
 *
 * <ul>
 * <li>{@code $L} emits a <em>literal</em> value with no escaping. Arguments for
 * literals may be strings, primitives, {@linkplain TypeSpec type declarations},
 * {@linkplain AnnotationSpec annotations} and even other code blocks.
 * <li>{@code $N} emits a <em>name</em>, using name collision avoidance where
 * necessary. Arguments for names may be strings (actually any
 * {@linkplain CharSequence character sequence}), {@linkplain ParameterSpec
 * parameters}, {@linkplain FieldSpec fields}, {@linkplain MethodSpec methods},
 * and {@linkplain TypeSpec types}.
 * <li>{@code $S} escapes the value as a <em>string</em>, wraps it with double
 * quotes, and emits that. For example, {@code 6" sandwich} is emitted
 * {@code "6\" sandwich"}.
 * <li>{@code $T} emits a <em>type</em> reference. Types will be imported if
 * possible. Arguments for types may be {@linkplain Class classes},
 * {@linkplain javax.lang.model.type.TypeMirror ,* type mirrors}, and
 * {@linkplain javax.lang.model.element.Element elements}.
 * <li>{@code $$} emits a dollar sign.
 * <li>{@code $W} emits a space or a newline, depending on its position on the
 * line. This prefers to wrap lines before 100 columns.
 * <li>{@code $Z} acts as a zero-width space. This prefers to wrap lines before
 * 100 columns.
 * <li>{@code $>} increases the indentation level.
 * <li>{@code $<} decreases the indentation level.
 * <li>{@code $[} begins a statement. For multiline statements, every line after
 * the first line is double-indented.
 * <li>{@code $]} ends a statement.
 * </ul>
 */
public final class CodeBlock {
	private static final Pattern NAMED_ARGUMENT = Pattern.compile("\\$(?<argumentName>[\\w_]+):(?<typeChar>[\\w]).*");
	private static final Pattern LOWERCASE = Pattern.compile("[a-z]+[\\w_]*");

	/** A heterogeneous list containing string literals and value placeholders. */
	final List<String> formatParts;
	final List<Object> args;

	private CodeBlock(Builder builder) {
		this.formatParts = Util.immutableList(builder.formatParts);
		this.args = Util.immutableList(builder.args);
	}

	public boolean isEmpty() {
		return formatParts.isEmpty();
	}

	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null)
			return false;
		if (getClass() != o.getClass())
			return false;
		return toString().equals(o.toString());
	}

	@Override
	public int hashCode() {
		return toString().hashCode();
	}

	@Override
	public String toString() {
		StringBuilder out = new StringBuilder();
		try {
			new CodeWriter(out).emit(this);
			return out.toString();
		} catch (IOException e) {
			throw new AssertionError();
		}
	}

	public static CodeBlock of(String format, Object... args) {
		return new Builder().add(format, args).build();
	}

	/**
	 * Joins {@code codeBlocks} into a single {@link CodeBlock}, each separated by
	 * {@code separator}. For example, joining {@code String s}, {@code Object o}
	 * and {@code int i} using {@code ", "} would produce
	 * {@code String s, Object o, int i}.
	 */
	public static CodeBlock join(Iterable<CodeBlock> codeBlocks, String separator) {
		return StreamSupport.stream(codeBlocks.spliterator(), false).collect(joining(separator));
	}

	/**
	 * A {@link Collector} implementation that joins {@link CodeBlock} instances
	 * together into one separated by {@code separator}. For example, joining
	 * {@code String s}, {@code Object o} and {@code int i} using {@code ", "} would
	 * produce {@code String s, Object o, int i}.
	 */
	public static Collector<CodeBlock, ?, CodeBlock> joining(String separator) {
		return Collector.of(() -> new CodeBlockJoiner(separator, builder()), CodeBlockJoiner::add,
				CodeBlockJoiner::merge, CodeBlockJoiner::join);
	}

	/**
	 * A {@link Collector} implementation that joins {@link CodeBlock} instances
	 * together into one separated by {@code separator}. For example, joining
	 * {@code String s}, {@code Object o} and {@code int i} using {@code ", "} would
	 * produce {@code String s, Object o, int i}.
	 */
	public static Collector<CodeBlock, ?, CodeBlock> joining(String separator, String prefix, String suffix) {
		Builder builder = builder().add("$N", prefix);
		return Collector.of(() -> new CodeBlockJoiner(separator, builder), CodeBlockJoiner::add, CodeBlockJoiner::merge,
				joiner -> {
					builder.add(CodeBlock.of("$N", suffix));
					return joiner.join();
				});
	}

	public static Builder builder() {
		return new Builder();
	}

	public Builder toBuilder() {
		Builder builder = new Builder();
		builder.formatParts.addAll(formatParts);
		builder.args.addAll(args);
		return builder;
	}

	public static final class Builder {
		final List<String> formatParts = new ArrayList<>();
		final List<Object> args = new ArrayList<>();

		private Builder() {
		}

		public boolean isEmpty() {
			return formatParts.isEmpty();
		}

		/**
		 * Adds code using named arguments.
		 *
		 * <p>
		 * Named arguments specify their name after the '$' followed by : and the
		 * corresponding type character. Argument names consist of characters in
		 * {@code a-z, A-Z, 0-9, and _} and must start with a lowercase character.
		 *
		 * <p>
		 * For example, to refer to the type {@link java.lang.Integer} with the argument
		 * name {@code
		 * clazz} use a format string containing {@code $clazz:T} and include the key
		 * {@code clazz} with value {@code java.lang.Integer.class} in the argument map.
		 */
		public Builder addNamed(String format, Map<String, ?> arguments) {
			int p = 0;

			for (String argument : arguments.keySet()) {
				checkArgument(LOWERCASE.matcher(argument).matches(),
						"argument '%s' must start with a lowercase character", argument);
			}

			while (p < format.length()) {
				int nextP = format.indexOf("$", p);
				if (nextP == -1) {
					formatParts.add(format.substring(p));
					break;
				}

				if (p != nextP) {
					formatParts.add(format.substring(p, nextP));
					p = nextP;
				}

				Matcher matcher = null;
				int colon = format.indexOf(':', p);
				if (colon != -1) {
					int endIndex = Math.min(colon + 2, format.length());
					matcher = NAMED_ARGUMENT.matcher(format.substring(p, endIndex));
				}
				if (matcher != null && matcher.lookingAt()) {
					String argumentName = matcher.group("argumentName");
					checkArgument(arguments.containsKey(argumentName), "Missing named argument for $%s", argumentName);
					char formatChar = matcher.group("typeChar").charAt(0);
					addArgument(format, formatChar, arguments.get(argumentName));
					formatParts.add("$" + formatChar);
					p += matcher.regionEnd();
				} else {
					checkArgument(p < format.length() - 1, "dangling $ at end");
					checkArgument(isNoArgPlaceholder(format.charAt(p + 1)), "unknown format $%s at %s in '%s'",
							format.charAt(p + 1), p + 1, format);
					formatParts.add(format.substring(p, p + 2));
					p += 2;
				}
			}

			return this;
		}

		/**
		 * Add code with positional or relative arguments.
		 *
		 * <p>
		 * Relative arguments map 1:1 with the placeholders in the format string.
		 *
		 * <p>
		 * Positional arguments use an index after the placeholder to identify which
		 * argument index to use. For example, for a literal to reference the 3rd
		 * argument: "$3L" (1 based index)
		 *
		 * <p>
		 * Mixing relative and positional arguments in a call to add is invalid and will
		 * result in an error.
		 */
		public Builder add(String format, Object... args) {
			boolean hasRelative = false;
			boolean hasIndexed = false;

			int relativeParameterCount = 0;
			int[] indexedParameterCount = new int[args.length];

			for (int p = 0; p < format.length();) {
				if (format.charAt(p) != '$') {
					int nextP = format.indexOf('$', p + 1);
					if (nextP == -1)
						nextP = format.length();
					formatParts.add(format.substring(p, nextP));
					p = nextP;
					continue;
				}

				p++; // '$'.

				// Consume zero or more digits, leaving 'c' as the first non-digit char after
				// the '$'.
				int indexStart = p;
				char c;
				do {
					checkArgument(p < format.length(), "dangling format characters in '%s'", format);
					c = format.charAt(p++);
				} while (c >= '0' && c <= '9');
				int indexEnd = p - 1;

				// If 'c' doesn't take an argument, we're done.
				if (isNoArgPlaceholder(c)) {
					checkArgument(indexStart == indexEnd, "$$, $>, $<, $[, $], $W, and $Z may not have an index");
					formatParts.add("$" + c);
					continue;
				}

				// Find either the indexed argument, or the relative argument. (0-based).
				int index;
				if (indexStart < indexEnd) {
					index = Integer.parseInt(format.substring(indexStart, indexEnd)) - 1;
					hasIndexed = true;
					if (args.length > 0) {
						indexedParameterCount[index % args.length]++; // modulo is needed, checked below anyway
					}
				} else {
					index = relativeParameterCount;
					hasRelative = true;
					relativeParameterCount++;
				}

				checkArgument(index >= 0 && index < args.length,
						"index %d for '%s' not in range (received %s arguments)", index + 1,
						format.substring(indexStart - 1, indexEnd + 1), args.length);
				checkArgument(!hasIndexed || !hasRelative, "cannot mix indexed and positional parameters");

				addArgument(format, c, args[index]);

				formatParts.add("$" + c);
			}

			if (hasRelative) {
				checkArgument(relativeParameterCount >= args.length, "unused arguments: expected %s, received %s",
						relativeParameterCount, args.length);
			}
			if (hasIndexed) {
				List<String> unused = new ArrayList<>();
				for (int i = 0; i < args.length; i++) {
					if (indexedParameterCount[i] == 0) {
						unused.add("$" + (i + 1));
					}
				}
				String s = unused.size() == 1 ? "" : "s";
				checkArgument(unused.isEmpty(), "unused argument%s: %s", s, String.join(", ", unused));
			}
			return this;
		}

		private boolean isNoArgPlaceholder(char c) {
			return c == '$' || c == '>' || c == '<' || c == '[' || c == ']' || c == 'W' || c == 'Z';
		}

		private void addArgument(String format, char c, Object arg) {
			switch (c) {
			case 'N':
				this.args.add(argToName(arg));
				break;
			case 'L':
				this.args.add(argToLiteral(arg));
				break;
			case 'S':
				this.args.add(argToString(arg));
				break;
			case 'T':
				this.args.add(argToType(arg));
				break;
			default:
				throw new IllegalArgumentException(String.format("invalid format string: '%s'", format));
			}
		}

		private String argToName(Object o) {
			if (o instanceof CharSequence)
				return o.toString();
			if (o instanceof ParameterSpec)
				return ((ParameterSpec) o).name;
			if (o instanceof FieldSpec)
				return ((FieldSpec) o).name;
			if (o instanceof MethodSpec)
				return ((MethodSpec) o).name;
			if (o instanceof TypeSpec)
				return ((TypeSpec) o).name;
			throw new IllegalArgumentException("expected name but was " + o);
		}

		private Object argToLiteral(Object o) {
			return o;
		}

		private String argToString(Object o) {
			return o != null ? String.valueOf(o) : null;
		}

		private TypeName argToType(Object o) {
			if (o instanceof TypeName)
				return (TypeName) o;
			if (o instanceof TypeMirror)
				return TypeName.get((TypeMirror) o);
			if (o instanceof Element)
				return TypeName.get(((Element) o).asType());
			if (o instanceof Type)
				return TypeName.get((Type) o);
			throw new IllegalArgumentException("expected type but was " + o);
		}

		/**
		 * @param controlFlow the control flow construct and its code, such as "if (foo
		 *                    == 5)". Shouldn't contain braces or newline characters.
		 */
		public Builder beginControlFlow(String controlFlow, Object... args) {
			add(controlFlow + " {\n", args);
			indent();
			return this;
		}

		/**
		 * @param controlFlow the control flow construct and its code, such as "else if
		 *                    (foo == 10)". Shouldn't contain braces or newline
		 *                    characters.
		 */
		public Builder nextControlFlow(String controlFlow, Object... args) {
			unindent();
			add("} " + controlFlow + " {\n", args);
			indent();
			return this;
		}

		public Builder endControlFlow() {
			unindent();
			add("}\n");
			return this;
		}

		/**
		 * @param controlFlow the optional control flow construct and its code, such as
		 *                    "while(foo == 20)". Only used for "do/while" control
		 *                    flows.
		 */
		public Builder endControlFlow(String controlFlow, Object... args) {
			unindent();
			add("} " + controlFlow + ";\n", args);
			return this;
		}

		public Builder addStatement(String format, Object... args) {
			add("$[");
			add(format, args);
			add(";\n$]");
			return this;
		}

		public Builder addStatement(CodeBlock codeBlock) {
			return addStatement("$L", codeBlock);
		}

		public Builder add(CodeBlock codeBlock) {
			formatParts.addAll(codeBlock.formatParts);
			args.addAll(codeBlock.args);
			return this;
		}

		public Builder indent() {
			this.formatParts.add("$>");
			return this;
		}

		public Builder unindent() {
			this.formatParts.add("$<");
			return this;
		}

		public Builder clear() {
			formatParts.clear();
			args.clear();
			return this;
		}

		public CodeBlock build() {
			return new CodeBlock(this);
		}
	}

	private static final class CodeBlockJoiner {
		private final String delimiter;
		private final Builder builder;
		private boolean first = true;

		CodeBlockJoiner(String delimiter, Builder builder) {
			this.delimiter = delimiter;
			this.builder = builder;
		}

		CodeBlockJoiner add(CodeBlock codeBlock) {
			if (!first) {
				builder.add(delimiter);
			}
			first = false;

			builder.add(codeBlock);
			return this;
		}

		CodeBlockJoiner merge(CodeBlockJoiner other) {
			CodeBlock otherBlock = other.builder.build();
			if (!otherBlock.isEmpty()) {
				add(otherBlock);
			}
			return this;
		}

		CodeBlock join() {
			return builder.build();
		}
	}
}
